Skip to main content
Glama

Cursor Talk to Figma MCP

by andreycretsu
ui.html27.5 kB
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Cursor MCP Plugin</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; color: #222222; background-color: #fafbfc; } .container { display: flex; flex-direction: column; height: 100%; } h1 { font-size: 16px; font-weight: 600; margin-bottom: 10px; color: #222222; } h2 { font-size: 14px; font-weight: 600; margin-top: 20px; margin-bottom: 8px; color: #222222; } button { background-color: #18a0fb; border: none; color: white; padding: 0 16px; border-radius: 6px; margin-top: 0; margin-bottom: 0; cursor: pointer; font-size: 14px; transition: background-color 0.2s; height: 40px; display: flex; align-items: center; } button:hover { background-color: #0d8ee0; } button.secondary { background-color: #f3f3f3; color: #222222; border: 1px solid #d1d5db; } button.secondary:hover { background-color: #e5e7eb; } button:disabled { background-color: #e5e7eb; color: #bdbdbd; cursor: not-allowed; } input { border: 1px solid #d1d5db; border-radius: 4px; padding: 8px; margin-bottom: 12px; font-size: 14px; width: 100%; box-sizing: border-box; background-color: #fff; color: #222222; height: 40px; } label { display: block; margin-bottom: 4px; font-size: 12px; font-weight: 500; color: #444444; } .status { margin-top: 16px; padding: 12px; border-radius: 6px; font-size: 14px; } .status.connected { background-color: #e6f9ed; color: #15803d; } .status.disconnected { background-color: #fdeaea; color: #b91c1c; } .status.info { background-color: #eaf4fd; color: #2563eb; } .section { margin-bottom: 24px; } .hidden { display: none; } .logo { width: 50px; height: 50px; } .header { display: flex; align-items: center; margin-bottom: 16px; } .header-text { margin-left: 12px; } .header-text h1 { margin: 0; font-size: 16px; } .header-text p { margin: 4px 0 0 0; font-size: 12px; color: #666666; } .tabs { display: flex; border-bottom: 1px solid #d1d5db; margin-bottom: 16px; } .tab { padding: 8px 16px; cursor: pointer; font-size: 14px; font-weight: 500; color: #666666; background: none; } .tab.active { border-bottom: 2px solid #18a0fb; color: #18a0fb; background: none; } .tab-content { display: none; } .tab-content.active { display: block; } .link { color: #18a0fb; text-decoration: none; cursor: pointer; } .link:hover { text-decoration: underline; } .header-logo { border-radius: 10px; padding: 0; background: none; display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; overflow: hidden; } .header-logo-image { width: 24px; height: 24px; object-fit: contain; } /* Progress styles */ .operation-complete { color: #15803d; } .operation-error { color: #b91c1c; } .badge { display: inline-block; background: #eaf4fd; color: #2563eb; border-radius: 12px; padding: 4px 10px; margin: 2px 4px 2px 0; font-size: 12px; font-weight: 500; border: 1px solid #b6d4fa; vertical-align: middle; } ol { margin-left: 0; padding-left: 20px; } .section ol { margin-left: 0; padding-left: 20px; } .section li { margin-left: 0; padding-left: 0; margin-bottom: 10px; } .section > div[style*="display: flex"] { align-items: center; } .section > div[style*="display: flex"] input { width: 160px; margin-bottom: 0; } .section > div[style*="display: flex"] button { margin-left: 12px; } .selection-box { margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f6fa; border: 1px solid #d1d5db; color: #222; font-size: 14px; } .selection-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } .selection-refresh { background: none; color: #2563eb; border: none; border-radius: 4px; padding: 0 4px; font-size: 13px; cursor: pointer; text-decoration: underline; transition: color 0.2s; margin-left: 0; } .selection-refresh:hover { color: #0d8ee0; background: none; } </style> </head> <body> <div class="container"> <div class="header"> <div class="header-logo" style="background: none; padding: 0; border-radius: 0;"> <svg width="40" height="40" viewBox="0 0 280 280" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect width="280" height="280" fill="#EDF2F7"/> <path d="M68.2937 144.584C84.2579 192.029 136.839 217.934 185.724 202.435C201.992 197.272 215.799 188.172 226.403 176.584C216.193 199.59 196.238 218.391 169.778 226.775C120.893 242.274 68.3208 216.377 52.3478 168.933C41.6905 137.273 50.03 104.03 71.2738 80.8089C62.6204 100.307 60.9651 122.822 68.2937 144.584ZM133.268 88.4061C112.097 78.6433 86.7813 87.3859 76.7234 107.94C73.3768 114.777 72.142 122.061 72.7595 129.094C68.133 118.607 67.99 106.312 73.4304 95.1889C83.4884 74.6434 108.804 65.8918 129.975 75.6546C144.096 82.169 152.811 95.2695 154.055 109.372C150.127 100.477 142.978 92.8892 133.268 88.4061ZM173.617 150.016C198.842 128.119 200.999 90.5358 178.449 66.0529C170.941 57.9098 161.635 52.2455 151.639 49.1136C168.614 48.9972 185.562 55.6906 197.768 68.9342C220.327 93.4081 218.171 131 192.945 152.888C176.122 167.492 153.277 171.42 133.241 165.147C147.63 165.049 162.046 160.056 173.617 150.016Z" fill="#0079C5"/> <path d="M169.653 112.832C171.094 126.38 167.219 139.266 159.658 149.547C175.022 138.63 184.168 120.322 182.064 100.626C178.923 71.0967 151.703 49.6295 121.278 52.6809C104.813 54.3364 90.7818 62.8374 81.8691 74.9624C89.5291 69.5218 98.7373 65.9156 108.857 64.8955C139.282 61.8441 166.503 83.3022 169.653 112.832ZM136.195 194.477C175.667 206.271 217.518 184.786 229.67 146.478C236.247 125.754 232.775 104.349 221.948 87.195C224.873 99.3111 224.633 112.313 220.587 125.065C208.436 163.364 166.593 184.858 127.121 173.064C109.01 167.65 94.558 156.169 85.3773 141.637C91.2469 165.914 109.869 186.612 136.195 194.477Z" fill="#00B950"/> </svg> </div> <div class="header-text"> <h1>Connect Figma to Cursor</h1> </div> </div> <div class="tabs"> <div id="tab-connection" class="tab active">Connection</div> <div id="tab-about" class="tab">About</div> </div> <div id="content-connection" class="tab-content active"> <div class="section"> <label for="port">WebSocket Server Port</label> <div style="display: flex; gap: 8px"> <input type="number" id="port" placeholder="3055" value="3055" min="1024" max="65535" /> <button id="btn-connect" class="primary">Connect</button> <button id="btn-disconnect" class="secondary" style="display: none;">Disconnect</button> </div> </div> <div id="connection-status" class="status disconnected"> Not connected to Cursor MCP server </div> <div id="selection-box" class="selection-box" style="display: none;"> <div class="selection-row"> <span id="selection-count"></span> <button id="refresh-selection" class="selection-refresh">Refresh</button> </div> <div id="selection-content" style="margin-top: 0;"></div> </div> <!-- Add Progress Bar Section --> <div id="progress-container" class="section hidden"> <h2>Operation Progress</h2> <div id="progress-message">No operation in progress</div> <div style="width: 100%; background-color: #444; border-radius: 4px; margin-top: 8px;"> <div id="progress-bar" style="width: 0%; height: 8px; background-color: #18a0fb; border-radius: 4px; transition: width 0.3s;"></div> </div> <div style="display: flex; justify-content: space-between; margin-top: 4px; font-size: 12px;"> <div id="progress-status">Not started</div> <div id="progress-percentage">0%</div> </div> </div> </div> <div id="content-about" class="tab-content"> <div class="section"> <p> This plugin allows Cursor AI to communicate with Figma </p> <ol> <li>Make sure the MCP server is running in Cursor</li> <li>Connect to the server using the port number (default: 3055)</li> <li>Make sure you run this command in your terminal: bunx cursor-talk-to-figma-mcp@latest</li> <li>In cursor terminal run bun setup and after this bun socket</li> <li>If something doesn't work just ask Andrey C</li> <li>Once connected, you can interact with Figma through Cursor using existing tools:</li> </ol> <div style="margin-top: 8px; margin-bottom: 8px;"> <span class="badge">get_document_info</span> <span class="badge">get_selection</span> <span class="badge">read_my_design</span> <span class="badge">get_node_info</span> <span class="badge">get_nodes_info</span> <span class="badge">create_rectangle</span> <span class="badge">create_frame</span> <span class="badge">create_text</span> <span class="badge">set_fill_color</span> <span class="badge">set_stroke_color</span> <span class="badge">move_node</span> <span class="badge">clone_node</span> <span class="badge">resize_node</span> <span class="badge">delete_node</span> <span class="badge">delete_multiple_nodes</span> <span class="badge">export_node_as_image</span> <span class="badge">set_text_content</span> <span class="badge">get_styles</span> <span class="badge">get_local_components</span> <span class="badge">get_annotations</span> <span class="badge">set_annotation</span> <span class="badge">set_multiple_annotations</span> <span class="badge">create_component_instance</span> <span class="badge">get_instance_overrides</span> <span class="badge">set_instance_overrides</span> <span class="badge">set_corner_radius</span> <span class="badge">scan_text_nodes</span> <span class="badge">scan_nodes_by_types</span> <span class="badge">set_multiple_text_contents</span> </div> </div> </div> </div> <script> // WebSocket connection state const state = { connected: false, socket: null, serverPort: 3055, pendingRequests: new Map(), channel: null, }; // UI Elements const portInput = document.getElementById("port"); const connectButton = document.getElementById("btn-connect"); const disconnectButton = document.getElementById("btn-disconnect"); const connectionStatus = document.getElementById("connection-status"); // Tabs const tabs = document.querySelectorAll(".tab"); const tabContents = document.querySelectorAll(".tab-content"); // Add UI elements for progress tracking const progressContainer = document.getElementById("progress-container"); const progressBar = document.getElementById("progress-bar"); const progressMessage = document.getElementById("progress-message"); const progressStatus = document.getElementById("progress-status"); const progressPercentage = document.getElementById("progress-percentage"); // Add selection elements const selectionBox = document.getElementById("selection-box"); const selectionContent = document.getElementById("selection-content"); const refreshSelectionBtn = document.getElementById("refresh-selection"); const selectionCount = document.getElementById("selection-count"); // Initialize UI function updateConnectionStatus(isConnected, message) { state.connected = isConnected; if (isConnected && state.channel) { connectionStatus.innerHTML = `join_channel <b>${state.channel}</b>`; } else { connectionStatus.innerHTML = message || (isConnected ? "Connected to Cursor MCP server" : "Not connected to Cursor MCP server"); } connectionStatus.className = `status ${ isConnected ? "connected" : "disconnected" }`; connectButton.disabled = isConnected; disconnectButton.disabled = !isConnected; portInput.disabled = isConnected; // Show/hide buttons in the row if (isConnected) { connectButton.style.display = "none"; disconnectButton.style.display = "inline-flex"; } else { connectButton.style.display = "inline-flex"; disconnectButton.style.display = "none"; } } // Connect to WebSocket server async function connectToServer(port) { try { if (state.connected && state.socket) { updateConnectionStatus(true, "Already connected to server"); return; } state.serverPort = port; state.socket = new WebSocket(`ws://localhost:${port}`); state.socket.onopen = () => { // Generate random channel name const channelName = generateChannelName(); console.log("Joining channel:", channelName); state.channel = channelName; // Join the channel using the same format as App.tsx state.socket.send( JSON.stringify({ type: "join", channel: channelName.trim(), }) ); }; state.socket.onmessage = (event) => { try { const data = JSON.parse(event.data); console.log("Received message:", data); if (data.type === "system") { // Successfully joined channel if (data.message && data.message.result) { state.connected = true; const channelName = data.channel; updateConnectionStatus( true, `Connected to server on port ${port} in channel: <strong>${channelName}</strong>` ); // Notify the plugin code parent.postMessage( { pluginMessage: { type: "notify", message: `Connected to Cursor MCP server on port ${port} in channel: ${channelName}`, }, }, "*" ); } } else if (data.type === "error") { console.error("Error:", data.message); updateConnectionStatus(false, `Error: ${data.message}`); state.socket.close(); } handleSocketMessage(data); } catch (error) { console.error("Error parsing message:", error); } }; state.socket.onclose = () => { state.connected = false; state.socket = null; updateConnectionStatus(false, "Disconnected from server"); }; state.socket.onerror = (error) => { console.error("WebSocket error:", error); updateConnectionStatus(false, "Connection error"); state.connected = false; state.socket = null; }; } catch (error) { console.error("Connection error:", error); updateConnectionStatus( false, `Connection error: ${error.message || "Unknown error"}` ); } } // Disconnect from websocket server function disconnectFromServer() { if (state.socket) { state.socket.close(); state.socket = null; state.connected = false; updateConnectionStatus(false, "Disconnected from server"); } } // Handle messages from the WebSocket async function handleSocketMessage(payload) { const data = payload.message; console.log("handleSocketMessage", data); // If it's a response to a previous request if (data.id && state.pendingRequests.has(data.id)) { const { resolve, reject } = state.pendingRequests.get(data.id); state.pendingRequests.delete(data.id); if (data.error) { reject(new Error(data.error)); } else { resolve(data.result); } return; } // If it's a new command if (data.command) { try { // Send the command to the plugin code parent.postMessage( { pluginMessage: { type: "execute-command", id: data.id, command: data.command, params: data.params, }, }, "*" ); } catch (error) { // Send error back to WebSocket sendErrorResponse( data.id, error.message || "Error executing command" ); } } } // Send a command to the WebSocket server async function sendCommand(command, params) { return new Promise((resolve, reject) => { if (!state.connected || !state.socket) { reject(new Error("Not connected to server")); return; } const id = generateId(); state.pendingRequests.set(id, { resolve, reject }); state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, command, params, }, }) ); // Set timeout to reject the promise after 30 seconds setTimeout(() => { if (state.pendingRequests.has(id)) { state.pendingRequests.delete(id); reject(new Error("Request timed out")); } }, 30000); }); } // Send success response back to WebSocket function sendSuccessResponse(id, result) { if (!state.connected || !state.socket) { console.error("Cannot send response: socket not connected"); return; } state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, result, }, }) ); } // Send error response back to WebSocket function sendErrorResponse(id, errorMessage) { if (!state.connected || !state.socket) { console.error("Cannot send error response: socket not connected"); return; } state.socket.send( JSON.stringify({ id, type: "message", channel: state.channel, message: { id, error: errorMessage, result: {} }, }) ); } // Helper to generate unique IDs function generateId() { return ( Date.now().toString(36) + Math.random().toString(36).substr(2, 5) ); } // Add this function after the generateId() function function generateChannelName() { const characters = "abcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < 8; i++) { result += characters.charAt( Math.floor(Math.random() * characters.length) ); } return result; } // Tab switching tabs.forEach((tab) => { tab.addEventListener("click", () => { tabs.forEach((t) => t.classList.remove("active")); tabContents.forEach((c) => c.classList.remove("active")); tab.classList.add("active"); const contentId = "content-" + tab.id.split("-")[1]; document.getElementById(contentId).classList.add("active"); }); }); // Connect to server connectButton.addEventListener("click", () => { const port = parseInt(portInput.value, 10) || 3055; updateConnectionStatus(false, "Connecting..."); connectionStatus.className = "status info"; connectToServer(port); }); // Disconnect from server disconnectButton.addEventListener("click", () => { updateConnectionStatus(false, "Disconnecting..."); connectionStatus.className = "status info"; disconnectFromServer(); }); // Function to update progress UI function updateProgressUI(progressData) { // Show progress container if hidden progressContainer.classList.remove("hidden"); // Update progress bar const progress = progressData.progress || 0; progressBar.style.width = `${progress}%`; progressPercentage.textContent = `${progress}%`; // Update message progressMessage.textContent = progressData.message || "Operation in progress"; // Update status text based on operation state if (progressData.status === 'started') { progressStatus.textContent = "Started"; progressStatus.className = ""; } else if (progressData.status === 'in_progress') { progressStatus.textContent = "In Progress"; progressStatus.className = ""; } else if (progressData.status === 'completed') { progressStatus.textContent = "Completed"; progressStatus.className = "operation-complete"; // Hide progress container after 5 seconds setTimeout(() => { progressContainer.classList.add("hidden"); }, 5000); } else if (progressData.status === 'error') { progressStatus.textContent = "Error"; progressStatus.className = "operation-error"; } } // Send operation progress update to server function sendProgressUpdateToServer(progressData) { if (!state.connected || !state.socket) { console.error("Cannot send progress update: socket not connected"); return; } console.log("Sending progress update to server:", progressData); state.socket.send( JSON.stringify({ id: progressData.commandId, type: "progress_update", channel: state.channel, message: { id: progressData.commandId, type: "progress_update", data: progressData } }) ); } // Reset progress UI function resetProgressUI() { progressContainer.classList.add("hidden"); progressBar.style.width = "0%"; progressMessage.textContent = "No operation in progress"; progressStatus.textContent = "Not started"; progressStatus.className = ""; progressPercentage.textContent = "0%"; } // Listen for messages from the plugin code window.onmessage = (event) => { const message = event.data.pluginMessage; if (!message) return; console.log("Received message from plugin:", message); switch (message.type) { case "connection-status": updateConnectionStatus(message.connected, message.message); break; case "auto-connect": connectButton.click(); break; case "auto-disconnect": disconnectButton.click(); break; case "command-result": // Forward the result from plugin code back to WebSocket sendSuccessResponse(message.id, message.result); break; case "command-error": // Forward the error from plugin code back to WebSocket sendErrorResponse(message.id, message.error); break; case "command_progress": // Update UI with progress information updateProgressUI(message); // Forward progress update to server sendProgressUpdateToServer(message); break; case "selection-info": updateSelectionBox(message.selection); break; } }; async function fetchSelection() { // Send a message to the plugin code to get selection parent.postMessage({ pluginMessage: { type: "get-selection" } }, "*"); } function updateSelectionBox(selection) { if (!selection || selection.selectionCount === 0) { selectionBox.style.display = "block"; selectionCount.textContent = "Nothing selected in Figma"; selectionContent.innerHTML = ""; } else { selectionBox.style.display = "block"; selectionCount.innerHTML = `<span style='color:#15803d;'>${selection.selectionCount} selected</span>`; let html = ""; for (const node of selection.selection) { const typeLabel = node.type.charAt(0) + node.type.slice(1).toLowerCase(); html += `<div><b>${node.name}</b> <span style='color:#666'>(${typeLabel})</span></div>`; } selectionContent.innerHTML = html; } } // Refresh selection on button click refreshSelectionBtn.addEventListener("click", fetchSelection); // Optionally, fetch selection on load fetchSelection(); </script> </body> </html>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/andreycretsu/cursor-talk-to-figma-mcp-main'

If you have feedback or need assistance with the MCP directory API, please join our Discord server